Skip to content

feat(schematics): rewrite Vertex AI imports to AI Logic on ng update - #3725

Merged
armando-navarro merged 3 commits into
angular:mainfrom
armando-navarro:a19-vertexai-ai-migration
Aug 3, 2026
Merged

feat(schematics): rewrite Vertex AI imports to AI Logic on ng update#3725
armando-navarro merged 3 commits into
angular:mainfrom
armando-navarro:a19-vertexai-ai-migration

Conversation

@armando-navarro

@armando-navarro armando-navarro commented Jul 29, 2026

Copy link
Copy Markdown
Collaborator

Adds an ng update migration that moves a workspace off the Vertex AI module onto Firebase AI Logic, and a guide for upgrading from AngularFire 20 to 21.

Background

AngularFire 21 renamed the Vertex AI module to Firebase AI Logic. The @angular/fire/vertexai entry point (and the older @angular/fire/vertexai-preview) were removed in favor of @angular/fire/ai, and the exported symbols were renamed. A project upgrading from 20 that used Vertex AI would fail to compile until it updated those imports by hand.

What this does

  • Extends the existing v21 migration (the one that aligns the firebase dependency) to also rewrite Vertex AI imports and their usages to AI Logic. It parses each source file with the TypeScript compiler and edits only real references, leaving strings, comments, and look-alike identifiers untouched. Named imports and aliases, namespace imports in value and type position, re-exports, and shorthand properties are handled.
  • getVertexAI is not treated as a rename. getAI coexisted with it in the old module, and plain getAI() defaults to the Gemini Developer API backend, so rewritten calls become getAI(app, { backend: new VertexAIBackend(location?) }) and keep the caller on the Vertex AI backend. Every rewritten site is logged with its file and line.
  • The overriding rule is that nothing changes behavior silently. Code the migration cannot rewrite with identical semantics is left in place with a per-site warning, and since the import path itself moves to the new entry point, the leftover code fails to compile right where the warning points. The list below is what that covers.
  • Direct firebase/vertexai imports (that entry point is also gone in SDK 12) migrate under the same rules.
  • Adds docs/version-21-upgrade.md, a note in docs/ai.md, and a README link.

Symbol map

Before (@angular/fire/vertexai) After (@angular/fire/ai)
getVertexAI(app?, { location? }) getAI(app, { backend: new VertexAIBackend(location?) })
provideVertexAI provideAI
VertexAI AI
VertexAIError AIError
VertexAIErrorCode AIErrorCode
VertexAIModel AIModel
VertexAIInstances AIInstances
vertexAIInstance$ AIInstance$
VertexAIModule AIModule

getGenerativeModel and getImagenModel keep their names. VertexAIOptions was removed rather than renamed (the new AIOptions takes a backend instead of a location), so imports of it are left in place and warned about.

Left for manual migration (each site gets its own warning)

  • getVertexAI calls whose arguments are not an optional app plus an optional literal { location } object, or whose options mention other rewritten symbols
  • getVertexAI handed around as a value or re-exported (rewriting either would silently change which backend its callers reach)
  • a local declaration shadowing an imported name (name-based rewriting cannot tell the two apart)
  • getAI or VertexAIBackend already bound from a source other than AI Logic (the rewrite cannot inject or reuse them safely)
  • export * from an old entry point (rewriting it would silently rename the file's re-exported public API)
  • files with syntax errors (an error-recovered parse has unreliable positions)

A file where a named getVertexAI import has any unrewritable use keeps every use of its named getVertexAI imports, so the pieces stay consistent. Namespace-style ns.getVertexAI(...) calls are judged per call.

Design notes

  • The migration code is organized under schematics/update/v21/vertexai-to-ai/ by responsibility: rename tables, shared interfaces, compiler resolution, safety analyses, the two scan passes, the getVertexAI edit builders, and orchestration.
  • typescript is a new optional peer dependency (>=5.8 <6.0), kept an esbuild external so the package does not grow by several megabytes, and resolved from the workspace when the rewrite first needs it (package resolution, then a workspace-root fallback for isolated layouts). It loads after the firebase alignment, so an unresolvable compiler costs only the rewrite and logs a warning. Verified in an environment with no typescript resolvable anywhere, and under pnpm's isolated node_modules layout.
  • applyEdits is exported only so its edit-conflict guard is unit-testable. ngUpdate's optional compiler parameter exists for the ESM test run, where require is unavailable.

Verification

  • 64 unit specs for this migration (the node suite totals 117) cover the rename shapes, every supported and unsupported getVertexAI call form, each left-for-manual case above, root handling (a root project with no sourceRoot, trailing-slash roots, node_modules exclusion, a null project entry), dedup of injected imports, log message content, deep-expression files, and an idempotent second run. I mutation-tested the guards: re-breaking them makes specs fail.
  • Ran the real flow against the final build, not just specs: scaffolded a fresh Angular 20 app importing @angular/fire/vertexai, installed the packed tarball, and ran ng update @angular/fire --migrate-only. The imports and the call rewrote (the location option moved into VertexAIBackend), firebase aligned to ^12.4.0, and a second run made no changes.

Refs #3686

AngularFire 21 renamed the Vertex AI module to Firebase AI Logic: the
@angular/fire/vertexai and older @angular/fire/vertexai-preview entry points
were removed in favor of @angular/fire/ai, and the exported symbols were renamed
(getVertexAI to getAI, provideVertexAI to provideAI, VertexAI to AI, and so on).
A workspace on 20 that used Vertex AI would fail to compile after the upgrade.

Extend the existing v21 migration (which aligns the firebase dependency) to also
rewrite these imports and their usages. The rewrite parses each source file with
the TypeScript compiler and edits only genuine references, so it leaves strings,
comments, and unrelated identifiers that merely share a name untouched. It
handles named imports and their aliases, namespace imports in both value and
type position, bare local re-exports, and shorthand properties.

The one accepted limitation is name shadowing: because the rewrite matches by
name, a local variable that shadows an imported name with the same spelling can
be mis-renamed. ng update always presents its changes as a diff for review, so
this is caught on inspection.

Also add typescript to the schematics esbuild externals so the compiler is
resolved from the workspace at ng-update time rather than bundled into the
package, matching how Angular's own migrations ship.

Docs: add a 20-to-21 upgrade guide, note the rename in the AI Logic guide, and
link the upgrade guide from the README.

Refs angular#3686

@tyler-reitz tyler-reitz left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice work on the AST approach — the two-pass design holds up well under poking. The namespace and identifier passes can't double-edit the same token (isMemberOrDeclaredName catches the child visit in both value and type position), applyEdits sorting back-to-front is right, and the spec coverage is genuinely thorough: shorthand expansion, accessor look-alikes, destructuring keys vs. binding initializers, idempotency. *.jasmine.ts is picked up by tools/jasmine.ts, so these run in CI. The symbol map matches the real src/ai/public_api.ts exports for every entry except one — which is the blocker below.

1. getVertexAIgetAI is not a rename, and the rewrite silently changes which backend the app calls

getVertexAI and getAI both existed in the old module. git show ac3dd7c^:src/vertexai/firebase.ts exports all four of getAI, getVertexAI, getGenerativeModel, getImagenModel. Two coexisting functions aren't a rename pair.

getAI() with no options defaults to the Google AI (Gemini Developer API) backend; the Vertex equivalent is getAI(app, { backend: new VertexAIBackend() }). So a user on Vertex who runs ng update ends up with code that compiles and then talks to a different API, with different enablement and billing. Nothing in the diff would look wrong on review, which defeats the "ng update always shows its changes as a diff" safety net cited under Known limitation.

Two ways out:

  • rewrite getVertexAI(...) to getAI(app, { backend: new VertexAIBackend() }), adding VertexAIBackend to the rewritten import, or
  • leave getVertexAI calls alone and context.logger.warn with a pointer to the upgrade guide.

Worth confirming the SDK's default backend against @firebase/ai before picking. The symbol table in docs/version-21-upgrade.md and the note added to docs/ai.md need the same correction — as written, both teach the rename as safe.

2. export { VertexAI } from '@angular/fire/vertexai' renames the user's public export

src/schematics/update/v21/vertexai-to-ai.ts:181collectSpecifierEdit rewrites importedNameNode, which is element.name when there's no propertyName. For a re-export with a from clause that produces export { AI } from '@angular/fire/ai', so the file's external export name changes from VertexAI to AI and downstream consumers break.

identifierUsageEdit already handles this correctly for the bare local re-export — expanding to export { AI as VertexAI }, with a good comment explaining why. The from-clause path should do the same. (export { VertexAI as Foo } from '...' is already correct, since propertyName is set.)

3. The migration walks node_modules when a project has no sourceRoot

rewriteVertexAIToAI derives roots from sourceRoot || root. A root project with root: "" and no sourceRoot — which older CLI versions generate — gives posix.join('/', '')/, and every path in the tree passes the prefix test. The content.includes(specifier) prefilter keeps most files from being parsed, but any dependency that re-exports @angular/fire/vertexai gets rewritten in place inside node_modules.

A filePath.includes('/node_modules/') guard is the usual fix and costs nothing.

4. Step ordering interacts badly with the typescript external

Making typescript an esbuild external is the right call for package size, but it isn't declared anywhere in src/package.json — unlike firebase-tools, which is an optional peer. Under a strict or isolated node_modules layout the top-level import * as ts fails at module load, which takes down the whole migration.

Because src/schematics/update/v21/index.ts:14 runs the rewrite before alignFirebaseVersion, that failure also costs users the firebase-12 alignment — the part they genuinely can't do without. Two small changes: declare typescript as an optional peer dependency, and run alignFirebaseVersion first so a rewrite failure can only cost you the rewrite.

5. Not covered: direct firebase/vertexai imports

Since this migration also moves users to Firebase JS SDK 12, where that entry point is gone, direct SDK imports break too. The guide flags it as manual, but the same AST pass would handle it with one more entry in OLD_MODULE_SPECIFIERS — worth considering, subject to the same getVertexAI caveat above.

Merge order

This is stacked on unmerged predecessor work, and it edits the same README feature-table region as #3724 (that PR re-flows the row boundaries around the last two cells; this one rewrites the Vertex AI cell's content). Whichever lands second will need a manual rebase.

@tyler-reitz tyler-reitz left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Marking this as changes-requested to hold the merge, per my earlier review.

The blocker is item 1: getVertexAI and getAI coexisted in the old module (git show ac3dd7c^:src/vertexai/firebase.ts exports both), so rewriting one to the other isn't a rename — it moves a Vertex user onto the Google AI backend silently, and the resulting diff looks correct on inspection. That needs resolving in the migration and in both docs pages before this lands.

Items 2-4 (the export { X } from public-export rename, the node_modules walk when sourceRoot is absent, and the typescript external / step-ordering interaction) are smaller but concrete. Item 5 is optional.

Happy to re-review once the backend question is settled.

getAI and getVertexAI coexisted in the old vertexai module and default
to different backends: plain getAI() talks to the Gemini Developer API,
so rewriting getVertexAI as a plain rename silently moved Vertex users
onto another Google API. Rewritten calls now become
getAI(app, { backend: new VertexAIBackend(location?) }), and every
rewritten site is logged with its file and line.

Anything the migration cannot rewrite with identical semantics is left
in place with a per-site warning, and the moved import path then fails
to compile, so nothing changes behavior silently. That covers
non-literal options, getVertexAI handed around as a value or
re-exported, local declarations that shadow a rewritten name, getAI or
VertexAIBackend bound from a non AI Logic source, star re-exports,
files with syntax errors, and removed symbols with no drop-in
successor (VertexAIOptions).

Also addressed from review: an un-aliased export { X } from an old
entry point keeps the file's public export name via an alias, a root
project without a sourceRoot is walked (with a node_modules guard),
and typescript is now an optional peer resolved from the workspace at
update time, loaded after the firebase alignment so a resolution
failure costs only the rewrite. Direct firebase/vertexai imports
migrate under the same rules.

The migration is reorganized from one file into
schematics/update/v21/vertexai-to-ai/ (rename tables, shared
interfaces, compiler resolution, safety analyses, the two scan passes,
edit builders, orchestration). applyEdits is exported only so its
edit-conflict guard is unit-testable, and ngUpdate's optional compiler
parameter exists for the ESM test run, where require is unavailable.
@armando-navarro

Copy link
Copy Markdown
Collaborator Author

Thanks Tyler. Item 1 caught something real, and the response grew well past the five items, so here is each one and then a summary of what else changed.

1. getVertexAI backend semantics

You were right, and I confirmed it against the SDK source before changing anything: getAI falls back to new GoogleAIBackend() when no backend is passed. I took the first exit you suggested.

  • Rewritten calls become getAI(app, { backend: new VertexAIBackend(location?) }), with a literal { location } moving into the constructor.
  • Only shapes I can prove safe are rewritten (no arguments, an app argument, or app plus a literal { location } object that mentions no other rewritten symbols).
  • Everything else is left in place with a per-site warning, and every rewritten call is logged with file and line.
  • Both docs pages now teach the real mapping, and the PR body's "known limitation" paragraph is gone in favor of an explicit left-for-manual list.

While auditing the old module's export surface for this, I also added VertexAIError, VertexAIErrorCode, and VertexAIModel to the rename map, and VertexAIOptions (removed with no drop-in successor, since the new AIOptions takes a backend) now warns with guidance instead of migrating into a broken import.

2. Public export names in re-exports

Fixed as you suggested: an un-aliased export { VertexAI } from '...' becomes export { AI as VertexAI } from '@angular/fire/ai', mirroring the bare local re-export case, and the aliased form keeps its public name as before. Spec added.

3. The node_modules walk

I tried to reproduce this before fixing it, and what I found is a different failure than we both expected. A root: "" project never reaches the join:

  • The truthiness filter on the line above drops the empty string first, and even a literal '/' root would match no files because the prefix test builds startsWith('//').
  • So instead of walking node_modules, that project was silently skipped and its files never migrated at all.
  • I fixed that (an empty root now maps to '/', and trailing-slash roots normalize) and added your node_modules guard, which becomes genuinely necessary once '/' is a legal root.
  • Specs cover both the previously-skipped shape and the guard. If I have misread the shape you hit, tell me and I will dig back in.

4. typescript as an external

Adopted both suggestions, with one addition I found while testing them.

  • typescript is now an optional peer (>=5.8 <6.0), and the firebase alignment runs first. But reordering alone could not isolate the failure you described: the bundle imports the rewrite module at its top level, so an unresolvable typescript failed at module load, before either step ran.
  • The compiler now resolves lazily when the rewrite first needs it (package resolution, then a workspace-root fallback for isolated layouts), and a failure warns and skips only the rewrite.
  • I verified both halves for real: in an environment with no typescript resolvable anywhere (the bundle loads, alignment runs, the rewrite warns and skips per file), and under pnpm's isolated node_modules, where the optional peer declaration makes the workspace's typescript resolvable and the rewrite runs end to end.

5. Direct firebase/vertexai imports

Adopted. firebase/vertexai and firebase/vertexai-preview rewrite to firebase/ai under exactly the same rules, including the backend treatment.

Beyond the five items

Your item 1 generalized into the rule the whole migration now follows:

  • Nothing changes behavior silently. Anything it cannot rewrite with identical semantics is left whole with a per-site warning, and since the import path moves, the leftover code fails to compile where the warning points.
  • Getting there added shadow detection for name collisions, a guard against reusing a getAI or VertexAIBackend bound from somewhere else, star re-exports left alone (rewriting one renames a file's re-exported public API), a skip for files with syntax errors, and a conflict check so overlapping text edits can never corrupt a file.
  • I mutation-tested those guards so the specs genuinely fail when they break.

The single file was getting long, so it is now a small module directory under update/v21/vertexai-to-ai/, organized by responsibility. The spec count for this area went from 19 to 64, and I re-ran the real ng update end to end on a fresh Angular 20 app against the final build: the rewrite is correct, the alignment applies, and a second run is a no-op.

This is a much bigger delta than the review asked for, so take whatever time it needs. If anything looks off, or you would rather see part of it split out, say the word.

@armando-navarro armando-navarro added bump: minor comp: ai Firebase AI Logic / Vertex AI (src/ai). comp: schematics ng add / deploy schematics (src/schematics). type: feature New capability or enhancement. labels Aug 3, 2026

@tyler-reitz tyler-reitz left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approving. I pulled the branch and verified rather than taking the summary on faith: npm run build succeeds, ng lint is clean apart from the pre-existing @ts-ignore in deploy/actions.ts, and the node suite is 117 specs / 0 failures, matching your numbers (60 in vertexai-to-ai.jasmine.ts plus 4 in index.jasmine.ts).

On item 3, you are right and my diagnosis was wrong. The original .filter((base: string) => !!base) drops '' before it reaches posix.join, so a root: "" project never produced a / root, and your second point holds too: startsWith(root + '/') builds '//' for a / root and matches nothing. There was no node_modules walk. The real bug was the inverse of what I described, a root project silently skipped and never migrated, and you found and fixed it. Your !filePath.split('/').includes('node_modules') is also better than the .includes('/node_modules/') I suggested, and it is genuinely load-bearing now that / is a legal root.

Item 1 checks out end to end. I confirmed options?.backend ?? new GoogleAIBackend() in @firebase/ai myself, so the original rewrite really did move callers onto the wrong backend. One detail your summary does not mention that I checked, because it decides whether the rewrite is truly behavior preserving: legacy @firebase/vertexai used DEFAULT_LOCATION = 'us-central1' and VertexAIBackend's constructor defaults to the same value, so getVertexAI(app) becoming getAI(app, { backend: new VertexAIBackend() }) keeps the region identical. The zero-argument case is right for the same kind of reason, since getAI's signature is getAI(app = getApp(), options) and an explicit undefined triggers the default parameter. Items 2, 4, and 5 all match what you describe in the code.

The "nothing changes behavior silently" rule is the right one to have landed on, and the per-site warnings plus the moved import path make the failures loud in the place the user needs to look.

One observation, not a request: this grew from roughly 460 lines to 2,394, and a good part of that is hardening beyond the blocker. It is the right hardening and I am not asking you to unpick it now. But a change that arrives at this size is hard to review as a unit, and next time the safety analyses and the optional item 5 work would probably be easier on a reviewer as a follow-up on top of the core fix. Worth keeping in mind rather than acting on here.

@armando-navarro
armando-navarro merged commit b551b5f into angular:main Aug 3, 2026
24 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bump: minor comp: ai Firebase AI Logic / Vertex AI (src/ai). comp: schematics ng add / deploy schematics (src/schematics). type: feature New capability or enhancement.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants